# ────────────────────────────────────────────────────
# Task_D v1: Python → C-###_claim_information.json (per claim)
# IN:  evidence_indexed.json
#      Fact_Ledger_base.json
#      claims_identified.json
#      claims_identified_case_type.json
#      evidence_actio_support.json
#      fact_actio_support.json
#      청구취지작성규칙_대여금청구.md (fallback: 금전의지급을구하는소_일반론.md)
#      청구취지작성규칙_구상금청구.md
#      청구취지작성규칙_보증채무금청구.md
#      청구취지작성규칙_사해행위취소청구.md
#      actio_pauliana_calc_v2.json (fallback: actio_pauliana_calc_v3.json)
# OUT: C-001_claim_information.json, C-002_claim_information.json, ...
# ────────────────────────────────────────────────────
- task_name: Task_D
  mcp: code-executor
  tool_name: run_code
  parameters:
    language: python
    requirements: "httpx"
    network: "agent-network"
    timeout: 180
    code: |
      #!/usr/bin/env python3
      from __future__ import annotations

      import copy
      import itertools
      import json
      import re
      import sys
      import unicodedata
      from typing import Any

      import httpx

      # =============================================================================
      # Task_D v1
      # - 기준 문서: claims_identified.json
      # - claim_id별로 개별 claim information JSON 생성
      # - claim_type -> case_kind 매핑
      # - source_fact_ids -> Fact_Ledger_base.json 조인
      # - evidence_refs -> evidence_indexed.json 조인
      # - 사해행위취소 사건은 evidence_actio_support / fact_actio_support 추가 조인
      # - case_kind별 청구취지 작성 규칙 자료를 relief_summary_statement_rule로 주입
      # - 사해행위취소 사건은 actio_pauliana_calc_v2.json(없으면 v3 fallback)을
      #   relief_summary_statement_further_rule로 추가 주입
      # =============================================================================

      LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
      MCP_HEADERS = {
          "Content-Type": "application/json",
          "Accept": "application/json, text/event-stream",
      }
      CLIENT = httpx.Client(timeout=60)

      MSG_ID_COUNTER = itertools.count(10)

      EVIDENCE_INDEX_PATTERN = re.compile(r"(E-\d{3})")
      EVIDENCE_ID_PATTERN = re.compile(r"^E-(\d{3})$")
      BO_ID_PATTERN = re.compile(r"^bh(\d+)$")


      def next_msg_id() -> int:
          return next(MSG_ID_COUNTER)


      def build_doc_name_candidates(*names: str) -> list[str]:
          candidates: list[str] = []
          for name in names:
              if not isinstance(name, str):
                  continue
              raw = name.strip()
              if not raw:
                  continue
              for variant in (raw, unicodedata.normalize("NFC", raw), unicodedata.normalize("NFD", raw)):
                  if variant not in candidates:
                      candidates.append(variant)
          return candidates


      RULE_DOC_NAME_CANDIDATES = {
          "대여금 청구": build_doc_name_candidates(
              "청구취지작성규칙_대여금청구.md",
              "금전의지급을구하는소_일반론.md",
          ),
          "구상금 청구": build_doc_name_candidates("청구취지작성규칙_구상금청구.md"),
          "보증채무금 청구": build_doc_name_candidates("청구취지작성규칙_보증채무금청구.md"),
          "사해행위취소 청구": build_doc_name_candidates("청구취지작성규칙_사해행위취소청구.md"),
      }

      ACTIO_FURTHER_RULE_DOC_CANDIDATES = build_doc_name_candidates(
          "actio_pauliana_calc_v2.json",
          "actio_pauliana_calc_v3.json",
      )


      # =============================================================================
      # MCP helpers (Task_A style)
      # =============================================================================

      def _init_mcp_session() -> None:
          response = CLIENT.post(
              LOCALDOCS_URL,
              json={
                  "jsonrpc": "2.0",
                  "id": 1,
                  "method": "initialize",
                  "params": {
                      "protocolVersion": "2025-03-26",
                      "capabilities": {},
                      "clientInfo": {"name": "stage-2-task-d-claim-info-v1", "version": "1.0"},
                  },
              },
              headers=MCP_HEADERS,
          )
          response.raise_for_status()
          session_id = response.headers.get("mcp-session-id")
          if session_id:
              MCP_HEADERS["mcp-session-id"] = session_id

          CLIENT.post(
              LOCALDOCS_URL,
              json={"jsonrpc": "2.0", "method": "notifications/initialized"},
              headers=MCP_HEADERS,
          ).raise_for_status()


      def _parse_mcp_response(text: str) -> dict[str, Any] | None:
          for line in text.strip().split("\n"):
              if line.startswith("data: "):
                  payload = line[6:]
                  try:
                      return json.loads(payload)
                  except Exception:
                      return None
          try:
              return json.loads(text)
          except Exception:
              return None


      def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]:
          response = CLIENT.post(
              LOCALDOCS_URL,
              json={
                  "jsonrpc": "2.0",
                  "id": msg_id,
                  "method": "tools/call",
                  "params": {"name": name, "arguments": arguments},
              },
              headers=MCP_HEADERS,
          )
          response.raise_for_status()
          parsed = _parse_mcp_response(response.text)
          if not parsed or "result" not in parsed:
              raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}")
          return parsed


      def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any:
          content = result["result"].get("content", [])
          text = content[0].get("text", "") if content else ""

          try:
              parsed = json.loads(text)
          except json.JSONDecodeError as exc:
              raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc

          if isinstance(parsed, dict) and "results" in parsed:
              results = parsed.get("results") or []
              if not results:
                  raise RuntimeError(f"No read_docs results for {doc_name}")
              inner = results[0].get("content", "")
              if inner == "" or inner is None:
                  raise RuntimeError(f"Empty content for {doc_name}")
              if isinstance(inner, str):
                  try:
                      return json.loads(inner)
                  except json.JSONDecodeError as exc:
                      raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc
              return inner

          return parsed


      def _extract_text_from_read_docs_result(result: dict[str, Any], doc_name: str) -> str:
          content = result["result"].get("content", [])
          text = content[0].get("text", "") if content else ""
          if not text:
              raise RuntimeError(f"Empty read_docs response for {doc_name}")

          try:
              parsed = json.loads(text)
          except json.JSONDecodeError:
              return text

          if isinstance(parsed, dict) and "results" in parsed:
              results = parsed.get("results") or []
              if not results:
                  raise RuntimeError(f"No read_docs results for {doc_name}")
              inner = results[0].get("content", "")
              if inner == "" or inner is None:
                  raise RuntimeError(f"Empty content for {doc_name}")
              if isinstance(inner, str):
                  return inner
              return json.dumps(inner, ensure_ascii=False, indent=2)

          if isinstance(parsed, str):
              return parsed
          return json.dumps(parsed, ensure_ascii=False, indent=2)


      def _read_required_json_doc(doc_name: str, msg_id: int) -> Any:
          result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id)
          return _extract_json_from_read_docs_result(result, doc_name)


      def _read_required_text_doc(doc_name: str, msg_id: int) -> str:
          result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id)
          return _extract_text_from_read_docs_result(result, doc_name)


      def _write_doc(doc_name: str, content: str, msg_id: int) -> None:
          _call_tool("write_file", {"path": doc_name, "content": content}, msg_id=msg_id)


      def _read_first_available_text_doc(doc_name_candidates: list[str]) -> tuple[str, str]:
          last_exc: Exception | None = None
          seen: set[str] = set()
          for doc_name in doc_name_candidates:
              if doc_name in seen:
                  continue
              seen.add(doc_name)
              try:
                  return doc_name, _read_required_text_doc(doc_name, msg_id=next_msg_id())
              except Exception as exc:
                  last_exc = exc
          raise RuntimeError(
              f"None of the text-doc candidates could be read: {doc_name_candidates}. Last error: {last_exc}"
          )


      def _read_first_available_json_doc(doc_name_candidates: list[str]) -> tuple[str, Any]:
          last_exc: Exception | None = None
          seen: set[str] = set()
          for doc_name in doc_name_candidates:
              if doc_name in seen:
                  continue
              seen.add(doc_name)
              try:
                  return doc_name, _read_required_json_doc(doc_name, msg_id=next_msg_id())
              except Exception as exc:
                  last_exc = exc
          raise RuntimeError(
              f"None of the JSON-doc candidates could be read: {doc_name_candidates}. Last error: {last_exc}"
          )


      # =============================================================================
      # Validation / normalization helpers
      # =============================================================================

      def ensure_object(value: Any, label: str) -> dict[str, Any]:
          if not isinstance(value, dict):
              raise ValueError(f"{label} must be a JSON object.")
          return value


      def ensure_array(value: Any, label: str) -> list[Any]:
          if not isinstance(value, list):
              raise ValueError(f"{label} must be a JSON array.")
          return value


      def ensure_string(value: Any, label: str) -> str:
          if not isinstance(value, str) or not value.strip():
              raise ValueError(f"{label} must be a non-empty string.")
          return value.strip()


      def to_string_list(value: Any) -> list[str]:
          if not isinstance(value, list):
              return []
          out: list[str] = []
          for item in value:
              if item is None:
                  continue
              text = str(item).strip()
              if text:
                  out.append(text)
          return out


      def unique_strings(values: list[str]) -> list[str]:
          seen: set[str] = set()
          out: list[str] = []
          for value in values:
              if value not in seen:
                  seen.add(value)
                  out.append(value)
          return out


      def parse_evidence_indexes(evidence_refs: list[str]) -> list[str]:
          found: list[str] = []
          for ref in evidence_refs:
              if not isinstance(ref, str):
                  continue
              found.extend(EVIDENCE_INDEX_PATTERN.findall(ref))
          return unique_strings(found)


      def evidence_sort_key(evidence_index: str) -> int:
          match = EVIDENCE_ID_PATTERN.match(evidence_index or "")
          return int(match.group(1)) if match else 10**9


      def bo_sort_key(source_bo_id: str) -> int:
          match = BO_ID_PATTERN.match(source_bo_id or "")
          return int(match.group(1)) if match else 10**9


      def is_actio_pauliana_case(case_kind: str) -> bool:
          return "사해행위취소" in case_kind


      def canonical_case_kind(case_kind: str) -> str:
          normalized = ensure_string(case_kind, "case_kind")
          if "사해행위취소" in normalized:
              return "사해행위취소 청구"
          if "보증채무금" in normalized:
              return "보증채무금 청구"
          if "구상금" in normalized:
              return "구상금 청구"
          if "대여금" in normalized:
              return "대여금 청구"
          return normalized


      # =============================================================================
      # Index builders
      # =============================================================================

      def build_case_kind_index(payload: dict[str, Any]) -> dict[str, str]:
          idx: dict[str, str] = {}

          if isinstance(payload.get("claim_types"), list):
              for entry in payload["claim_types"]:
                  entry = ensure_object(entry, "claim_types entry")
                  claim_id = ensure_string(entry.get("claim_id"), "claim_types[].claim_id")
                  claim_type = ensure_string(entry.get("claim_type"), "claim_types[].claim_type")
                  idx[claim_id] = claim_type
              return idx

          if isinstance(payload.get("claims_taxonomy"), list):
              for entry in payload["claims_taxonomy"]:
                  entry = ensure_object(entry, "claims_taxonomy entry")
                  claim_id = ensure_string(entry.get("claim_id"), "claims_taxonomy[].claim_id")
                  case_kind = entry.get("case_kind") or entry.get("macro_category")
                  idx[claim_id] = ensure_string(case_kind, f"case_kind for {claim_id}")
              return idx

          raise ValueError("claims_identified_case_type.json must have either claim_types[] or claims_taxonomy[].")


      def build_fact_index(payload: list[Any]) -> dict[str, dict[str, Any]]:
          idx: dict[str, dict[str, Any]] = {}
          for entry in ensure_array(payload, "Fact_Ledger_base"):
              entry = ensure_object(entry, "Fact_Ledger_base entry")
              fact_id = ensure_string(entry.get("fact_id"), "Fact_Ledger_base[].fact_id")
              idx[fact_id] = entry
          return idx


      def build_evidence_index(payload: list[Any]) -> dict[str, dict[str, Any]]:
          idx: dict[str, dict[str, Any]] = {}
          for entry in ensure_array(payload, "evidence_indexed"):
              entry = ensure_object(entry, "evidence_indexed entry")
              evidence_index = ensure_string(entry.get("evidence_index"), "evidence_indexed[].evidence_index")
              idx[evidence_index] = entry
          return idx


      def build_evidence_actio_index(payload: list[Any]) -> dict[str, dict[str, Any]]:
          idx: dict[str, dict[str, Any]] = {}
          for entry in ensure_array(payload, "evidence_actio_support"):
              entry = ensure_object(entry, "evidence_actio_support entry")
              evidence_index = ensure_string(entry.get("evidence_index"), "evidence_actio_support[].evidence_index")
              support = ensure_object(entry.get("actio_pauliana_support"), "evidence_actio_support[].actio_pauliana_support")
              idx[evidence_index] = copy.deepcopy(support)
          return idx


      def build_fact_actio_index(payload: list[Any]) -> dict[str, dict[str, Any]]:
          idx: dict[str, dict[str, Any]] = {}
          for entry in ensure_array(payload, "fact_actio_support"):
              entry = ensure_object(entry, "fact_actio_support entry")
              source_bo_id = ensure_string(entry.get("source_bo_id"), "fact_actio_support[].source_bo_id")
              support = ensure_object(entry.get("actio_pauliana_support"), "fact_actio_support[].actio_pauliana_support")
              idx[source_bo_id] = copy.deepcopy(support)
          return idx


      # =============================================================================
      # Rule-material loaders
      # =============================================================================

      def load_relief_summary_statement_rule(case_kind: str) -> list[str]:
          canonical = canonical_case_kind(case_kind)
          if canonical not in RULE_DOC_NAME_CANDIDATES:
              raise ValueError(f"Unsupported case_kind for relief_summary_statement_rule: {case_kind}")

          selected_doc, selected_text = _read_first_available_text_doc(RULE_DOC_NAME_CANDIDATES[canonical])
          print(
              f"[Task_D] relief_summary_statement_rule mapped: {canonical} -> {selected_doc}",
              file=sys.stderr,
          )
          return [selected_text]


      def load_relief_summary_statement_further_rule(case_kind: str) -> list[str] | None:
          if not is_actio_pauliana_case(case_kind):
              return None

          selected_doc, payload = _read_first_available_json_doc(ACTIO_FURTHER_RULE_DOC_CANDIDATES)
          print(
              f"[Task_D] relief_summary_statement_further_rule mapped: {case_kind} -> {selected_doc}",
              file=sys.stderr,
          )
          return [json.dumps(payload, ensure_ascii=False, indent=2)]


      # =============================================================================
      # Payload builders
      # =============================================================================

      def build_fact_block(fact_id: str, fact_index: dict[str, dict[str, Any]]) -> dict[str, Any]:
          if fact_id not in fact_index:
              raise ValueError(f"fact_id {fact_id} not found in Fact_Ledger_base.json")

          fact = fact_index[fact_id]
          return {
              "fact_id": fact_id,
              "source_bo_id": fact.get("source_bo_id"),
              "type": fact.get("type"),
              "date": fact.get("date"),
              "parties": copy.deepcopy(fact.get("parties")) if isinstance(fact.get("parties"), list) else [],
              "object_spec": fact.get("object_spec"),
              "amount": fact.get("amount"),
              "action": fact.get("action"),
              "evidence_refs": unique_strings(to_string_list(fact.get("evidence_refs"))),
              "credibility": fact.get("credibility"),
              "legal_calculation_object": copy.deepcopy(fact.get("legal_calculation_object")) if isinstance(fact.get("legal_calculation_object"), dict) else None,
          }


      def build_evidence_block(evidence_index: str, evidence_catalog_index: dict[str, dict[str, Any]]) -> dict[str, Any]:
          if evidence_index not in evidence_catalog_index:
              raise ValueError(f"evidence_index {evidence_index} not found in evidence_indexed.json")

          entry = evidence_catalog_index[evidence_index]
          return {
              "evidence_index": evidence_index,
              "doc_uid": entry.get("doc_uid"),
              "title": entry.get("title"),
              "title_normalized": entry.get("title_normalized"),
              "doc_type": entry.get("doc_type"),
              "key_facts": copy.deepcopy(entry.get("key_facts")) if isinstance(entry.get("key_facts"), list) else [],
              "key_dates": copy.deepcopy(entry.get("key_dates")) if isinstance(entry.get("key_dates"), list) else [],
              "key_amounts": copy.deepcopy(entry.get("key_amounts")) if isinstance(entry.get("key_amounts"), list) else [],
              "key_parties": copy.deepcopy(entry.get("key_parties")) if isinstance(entry.get("key_parties"), list) else [],
          }


      def build_claim_payload(
          claim_entry: dict[str, Any],
          case_kind: str,
          relief_summary_statement_rule: list[str],
          relief_summary_statement_further_rule: list[str] | None,
          fact_index: dict[str, dict[str, Any]],
          evidence_catalog_index: dict[str, dict[str, Any]],
          evidence_actio_index: dict[str, dict[str, Any]],
          fact_actio_index: dict[str, dict[str, Any]],
      ) -> dict[str, Any]:
          claim_id = ensure_string(claim_entry.get("claim_id"), "claim_id")
          source_fact_ids = unique_strings(to_string_list(claim_entry.get("source_fact_ids")))

          facts = [build_fact_block(fact_id, fact_index) for fact_id in source_fact_ids]

          evidence_indexes: list[str] = []
          source_bo_ids: list[str] = []
          for fact in facts:
              source_bo_id = fact.get("source_bo_id")
              if isinstance(source_bo_id, str) and source_bo_id.strip():
                  source_bo_ids.append(source_bo_id.strip())
              evidence_indexes.extend(parse_evidence_indexes(fact.get("evidence_refs") or []))

          evidence_indexes = sorted(unique_strings(evidence_indexes), key=evidence_sort_key)
          evidences = [build_evidence_block(evidence_index, evidence_catalog_index) for evidence_index in evidence_indexes]

          payload: dict[str, Any] = {
              "claim_id": claim_id,
              "case_kind": case_kind,
              "relief_summary_statement_rule": copy.deepcopy(relief_summary_statement_rule),
          }

          if relief_summary_statement_further_rule is not None:
              payload["relief_summary_statement_further_rule"] = copy.deepcopy(relief_summary_statement_further_rule)

          payload.update(
              {
                  "claim_title": claim_entry.get("claim_title"),
                  "plaintiffs": unique_strings(to_string_list(claim_entry.get("plaintiffs"))),
                  "defendants": unique_strings(to_string_list(claim_entry.get("defendants"))),
                  "claim_statement": claim_entry.get("claim_statement"),
                  "source_fact_ids": source_fact_ids,
                  "review_flags": unique_strings(to_string_list(claim_entry.get("review_flags"))),
                  "completeness": copy.deepcopy(claim_entry.get("completeness")),
                  "facts": facts,
                  "evidences": evidences,
              }
          )

          if is_actio_pauliana_case(case_kind):
              sorted_bo_ids = sorted(unique_strings(source_bo_ids), key=bo_sort_key)
              payload["evidence_actio_support"] = [
                  {
                      "evidence_index": evidence_index,
                      "actio_pauliana_support": copy.deepcopy(evidence_actio_index[evidence_index]),
                  }
                  for evidence_index in evidence_indexes
                  if evidence_index in evidence_actio_index
              ]
              payload["fact_actio_support"] = [
                  {
                      "source_bo_id": source_bo_id,
                      "actio_pauliana_support": copy.deepcopy(fact_actio_index[source_bo_id]),
                  }
                  for source_bo_id in sorted_bo_ids
                  if source_bo_id in fact_actio_index
              ]

          return payload


      # =============================================================================
      # Main
      # =============================================================================

      def main() -> None:
          _init_mcp_session()

          claims_identified = ensure_object(
              _read_required_json_doc("claims_identified.json", msg_id=next_msg_id()),
              "claims_identified.json",
          )
          claim_entries = ensure_array(claims_identified.get("identified_claims"), "identified_claims")

          claims_identified_case_type = ensure_object(
              _read_required_json_doc("claims_identified_case_type.json", msg_id=next_msg_id()),
              "claims_identified_case_type.json",
          )
          case_kind_index = build_case_kind_index(claims_identified_case_type)

          fact_ledger_base = _read_required_json_doc("Fact_Ledger_base.json", msg_id=next_msg_id())
          fact_index = build_fact_index(fact_ledger_base)

          evidence_indexed = _read_required_json_doc("evidence_indexed.json", msg_id=next_msg_id())
          evidence_catalog_index = build_evidence_index(evidence_indexed)

          evidence_actio_support = _read_required_json_doc("evidence_actio_support.json", msg_id=next_msg_id())
          evidence_actio_index = build_evidence_actio_index(evidence_actio_support)

          fact_actio_support = _read_required_json_doc("fact_actio_support.json", msg_id=next_msg_id())
          fact_actio_index = build_fact_actio_index(fact_actio_support)

          present_case_kinds: list[str] = []
          for raw_entry in claim_entries:
              claim_entry = ensure_object(raw_entry, "identified_claims entry")
              claim_id = ensure_string(claim_entry.get("claim_id"), "identified_claims[].claim_id")
              if claim_id not in case_kind_index:
                  raise ValueError(f"claim_id {claim_id} not found in claims_identified_case_type.json")
              present_case_kinds.append(canonical_case_kind(case_kind_index[claim_id]))
          present_case_kinds = unique_strings(present_case_kinds)

          relief_rule_index: dict[str, list[str]] = {}
          relief_further_rule_index: dict[str, list[str] | None] = {}
          for case_kind in present_case_kinds:
              relief_rule_index[case_kind] = load_relief_summary_statement_rule(case_kind)
              relief_further_rule_index[case_kind] = load_relief_summary_statement_further_rule(case_kind)

          written = 0
          for raw_entry in claim_entries:
              claim_entry = ensure_object(raw_entry, "identified_claims entry")
              claim_id = ensure_string(claim_entry.get("claim_id"), "identified_claims[].claim_id")
              if claim_id not in case_kind_index:
                  raise ValueError(f"claim_id {claim_id} not found in claims_identified_case_type.json")

              case_kind = case_kind_index[claim_id]
              canonical = canonical_case_kind(case_kind)

              payload = build_claim_payload(
                  claim_entry=claim_entry,
                  case_kind=case_kind,
                  relief_summary_statement_rule=relief_rule_index[canonical],
                  relief_summary_statement_further_rule=relief_further_rule_index.get(canonical),
                  fact_index=fact_index,
                  evidence_catalog_index=evidence_catalog_index,
                  evidence_actio_index=evidence_actio_index,
                  fact_actio_index=fact_actio_index,
              )

              output_name = f"{claim_id}_claim_information.json"
              rendered = json.dumps(payload, ensure_ascii=False, indent=2)
              _write_doc(output_name, rendered, msg_id=next_msg_id())
              written += 1

          print(f"[Task_D] Wrote {written} claim_information files", file=sys.stderr)


      if __name__ == "__main__":
          main()
